iT邦幫忙

2023 iThome 鐵人賽

DAY 29
0
Vue.js

業主說給你30天學會Vue系列 第 29

V29_Vue的小專案_youtube點播機(3)

  • 分享至 

  • xImage
  •  

V29_Vue的小專案_youtube點播機(3)

在上一篇的發文中,對於小專案youtube點播機
己完成了播放機的本體與清單

接下來要處理與播放有關的部份
隨機播放,順序播放,及上傳播放清單的功能

首先要處理的就是要取得播放影片的狀態
像是播放完成的事件
以及自動播放的功能

為了可以取得播放器的播放狀態,之前用<iframe>的方式只能播放,
無法得知是否播放完成了

因此,需要引入Youtube 的 IFrame Player API
官網的說明連結如下

YouTube 開發人員說明文件
https://developers.google.com/youtube/documentation?hl=zh-tw

IFrame Player API 參考資料
https://developers.google.com/youtube/iframe_api_reference?hl=zh-tw

YouTube 播放器參數
https://developers.google.com/youtube/player_parameters?hl=zh-tw

首先要透過IFrame Player API來建立一個 YT.Player 的物件
此物件會產生 <ifame> 的播放元件,這樣就可以完全控制 YT.Player 的操作,
也可以讀取播放器的狀態了

建立一個 YT.Player 的物件的程式碼如下

<!DOCTYPE html>
<html>
  <body>
   
    <div id="ytplayer"></div>

    <script>
      var tag = document.createElement('script');
      tag.src = "https://www.youtube.com/iframe_api";
      var firstScriptTag = document.getElementsByTagName('script')[0];
      firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);

      let player;
      function onYouTubeIframeAPIReady() {
        player = new YT.Player('ytplayer', {
          height: '390',
          width: '640',
          videoId: 'M7lc1UVf-VE',
          playerVars: {
            'playsinline': 1
          },
          events: {
            'onReady': onPlayerReady,
            'onStateChange': onPlayerStateChange
          }
        });
      }

      function onPlayerReady(event) {
        event.target.playVideo();
      }

      let done = false;
      let pid = 0;
      function onPlayerStateChange(event) {
        
        console.log(event.data);
        if (event.data == 0 ) {
          done = true;
        }

        if(done){
          done = false;
          $("#app")[0]._vnode.component.ctx.change_yt();
        }
      }

      function stopVideo() {
        player.stopVideo();
      }


    </script>
  </body>
</html>

步驟1: 建立預備給player用的 <div id="ytplayer"></div>

步驟2: 匯入 IFrame Player API

var tag = document.createElement('script');  
// 產生<script>

tag.src = "https://www.youtube.com/iframe_api"; 
// 設定 script的src 是 https://www.youtube.com/iframe_api 就是api的來源

var firstScriptTag = document.getElementsByTagName('script')[0]; 
// 找出第一個<script>的元素

firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);  
// 將新產生的<script>加在第一個<script>的元素的前面,至於為什麼要放在第一個<script>處,就先這樣了解就好

步驟3: 建立player元件

let player; 
// 宣告player變數

function onYouTubeIframeAPIReady() {  //當api載入完成時,執行以下程式
  
  player = new YT.Player('ytplayer', {   //建立 YT.Player() 元件,其中 'ytplayer' 對應到 <div id="ytplayer"></div> 的id
    height: '390',  // 設定播放器的 width,height,videoId 是預先要載入的影片的videoID
    width: '640',
    videoId: 'M7lc1UVf-VE',
    playerVars: {
      'playsinline': 1  //如下說明
    },
    events: {
      'onReady': onPlayerReady,   //設定2個常用的事件 onReady 播放器準備好時,
      'onStateChange': onPlayerStateChange // 播放器狀態改變時
    }
  });

}

playsinline 這個參數,參考官網的說明如下,看起來是針對 iOS 裝置的設定,
0 是指在iOS 裝置上以全螢幕模式播放
1 是指在行動瀏覽器以內嵌方式播放

不過對桌機瀏覽器來說,這個參數也可以先忽略

再來是 播放器的狀態 對照表

-1 – 尚未開始
 0 – 已結束
 1 – 播放中
 2 – 已暫停
 3 – 緩衝處理中
 5 – 提示的影片

所以要確認影片是否播完,可以檢查狀態是否為 0
就可以執行切換下一首的操作

步驟4: 設定事件要執行的動作

function onPlayerReady(event) {  // 當播放器準備好就開始播放影片
  event.target.playVideo();
}

let done = false;
let pid = 0;
function onPlayerStateChange(event) {  // 當播放器狀態改變時就執行
  
  console.log(event.data);  // 列出目前的狀態
  if (event.data == 0 ) {  // 若狀態代碼是0, 代表影片播完了
    done = true;    // 設定變數done 為true
  }

  if(done){    // 若done為true,執行切換影片的操作
    done = false;
    $("#app")[0]._vnode.component.ctx.change_yt();
  }
}

function stopVideo() {  // 設定影片停止的函式
  player.stopVideo();  // 執行停止播放
}

其中 $("#app")[0]._vnode.component.ctx.change_yt(); 的部份
是從 $("#app") 的查詢,找到詢此路徑可以呼叫到 在vue元件中設定的 method
也就是 change_yt() 的功能,會進行影片切換

切換的方式有隨機播放,也有輪播的方式
有時候元件有哪些功能可以使用,需要透過Chrome 的 DevTools來查看

console.log($("#app")) 會列出元件完整的 屬性,方法及樣式

這部份的程式是要寫在index.html本身的檔案上
不過為了版面的排列,將 <div id="ytplayer"></div> 移到 App.vue 的<template>裡面

另外,因為會用到 p5.js 及 jQuery,w3.css
還要加入

<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.2/p5.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
<link rel="stylesheet" href="https://www.w3schools.com/w3css/4/w3.css">

//------------------------------
接下是就是
App.vue的部份
原本的

<div>
 <iframe width="640" height="360" :src="ytlink" title="" frameborder="1" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
</div>

改成

<div>
  <div id="ytplayer"></div>
</div>

player = new YT.Player('ytplayer' {}) 建立後
就會代換為

<iframe id="ytplayer" frameborder="0" allowfullscreen="1" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" title="2 Hours Of Canon in D by Pachelbel (Most Popular Version) | Relaxing Music | Piano & Cello" width="640" height="360" src="https://www.youtube.com/embed/bHQqvYy5KYo?enablejsapi=1&origin=http%3A%2F%2Flocalhost%3A5173&widgetid=1"></iframe>

這時 <iframe> 的id變成 ytplayer

按鈕的配置作了一些調整

<button class="w3-btn w3-border w3-round-large" @click="add_yt">新增</button>
<button class="w3-btn w3-border w3-round-large" @click="record_yt">修改</button>
<button class="w3-btn w3-border w3-round-large" @click="delete_yt">刪除</button>
<button class="w3-btn w3-border w3-round-large" @click="change_yt">下一首</button>
<button class="w3-btn w3-border w3-round-large" @click="save_yt">下載清單</button>
@click="add_yt" -> 新增
@click="record_yt" -> 修改
@click="delete_yt" -> 刪除
@click="change_yt" -> 下一首
@click="save_yt" -> 下載清單

接下來針對有變動的部份進行說明

add_yt() 的部份

原本 this.ytlink = "https://www.youtube.com/embed/"+this.videoID+"?autoplay=1" 來綁定iframe 的src
改成 player.loadVideoById(this.videoID, 0); 就是載入videoID 來播放影片

setTimeout(()=>{ 
    this.vt_title = $("#ytplayer").attr("title"); 
    let yt_index = this.ytlist.findIndex((e) => e.videoID ===  this.videoID )
    if(yt_index == -1 && this.videoID.length==11){
      this.ytlist.push({videoID: this.videoID, videoTitle: this.vt_title, albumName: this.vt_album})
    }

    console.log($("#app")[0]._vnode.component.ctx.ytlist);
}, 2000);

因為 player.loadVideoById() 載入到iframe需要等待一下,就可以讀到 video的title標題了
因此用 setTimeout , 2秒後 讀到 video的title標題,然後加入到 播放清單 ytlist

edit_yt(n){
  console.log(n)
  this.videoID = n.videoID
  this.vt_title = n.videoTitle
  this.vt_album = n.albumName
  this.ytlink = "https://www.youtube.com/embed/"+this.videoID+"?autoplay=1"
  player.loadVideoById(this.videoID, 0);
}

同樣改成 player.loadVideoById(this.videoID, 0);
第1個參數 是 videoID,第2個參數時播放的開始秒數

change_yt(){
  let v_cnt = this.ytlist.length;  
  // 先取得目前播放清單 ytlist 的數量

  let vid = parseInt(Math.random()*v_cnt+""); 
  // 隨機取得一個編號  Math.random() 是 0~ 1, v_cnt是數量,Math.random()*v_cnt 是 0~ v_cnt
  // Math.random()*v_cnt+"" 轉成字串, parseInt() 是取整數,也就是當作 索引值

  this.edit_yt(this.ytlist[vid]) 
  // this.ytlist[vid] 取的該索引值的播放資料 
  // 例如 { videoID: "Ptk_1Dc2iPY", videoTitle: "Canon in D - Cello & Piano", albumName: "Canon"},
  // 然後 執行 this.edit_yt() 執行影片播放
}

以上的方法是 隨機播放的方式

若是要播輪的話
可以改成

let vid = 0; 
change_yt(){
  let v_cnt = this.ytlist.length;  
  let vid = (vid+1)%v_cnt; 
  // 取得下一首資料
  this.edit_yt(this.ytlist[vid]) 
}

在下一首的計算上,有不用的規則
在新增時 會切換到 最新的影片
因此 this.vid = this.ytlist.length-1; 所以vid是影片數量減1

在刪除時 會停止目前的影片,切到下一首,
原則止vid不變,但影片數量少1, 所以 vid 要重新設定
變成

let v_cnt = this.ytlist.length;
this.vid = (this.vid)%v_cnt;  
// 要特別這樣計算是因為這個狀況
// 若影片數量是 4, vid是3, 當3被刪除時 若 vid繼續是3, 會變成讀不到3
// 因為數量變成 3, 所以 3%3=0, vid要變成0, 才讀的到
// 也就是最後1筆刪除時,下一首就是跳回第1首了

this.edit_yt(this.ytlist[this.vid])

在修改時,會先點選其中一首
這時vid要改成該首的索引值

let yt_index = this.ytlist.findIndex((e) => e.videoID ===  this.videoID )
// 取的索引值

this.vid = yt_index; 

最後是上傳播放清單的部份,
原則上是以下載的清單格式為準
也就是 JSON的格式,
下載後,可以根據JSON格式。進行清單的編輯,
然後再以將JSON清單內容檔案上傳上去,
同時更新成新的 播放清單

先新增
上傳清單: <input type="file" class="w3-btn w3-border w3-round-large" id="jsnfile" name="jsnfile" @change="upload_yt" accept="*.json">

按下 「選擇檔案」 按鈕來挑選 .json 檔案,
當挑選完檔案後,就會觸發 @change 執行 upload_yt

accept=".json" 可以設定限制只能上傳 .json 檔案
接下來是 upload_yt()

async upload_yt(e){
  const file = e.target.files.item(0)
  this.jsn_data = await file.text();
  console.log(JSON.parse(this.jsn_data))
  this.ytlist = JSON.parse(this.jsn_data)
  this.vid = 0
  this.edit_yt(this.ytlist[this.vid]) 
}

最後還有一個需求
就是若只有videoID, 沒有 videoTitle 時
可以進行標題查詢,寫回 videoTitle 及 vt_title

edit_yt(n){  // n 代表點選到的影片資料
	player.loadVideoById(this.videoID, 0); //切換影片後,等2秒
	setTimeout(()=>{ 
	    this.vt_title = $("#ytplayer").attr("title"); // 取得影片的標題,並傳回 vt_title
	    n.videoTitle = this.vt_title   // 同時也傳回 videoTitle
	}, 2000);
}

補充一下 save_yt() 的部份
在設定下載清單的檔名時,為了不要只用同一個 youtube_list.json 的檔名
另外增加以當下時間為檔名的處理方式

save_yt(){
  console.log(top)
  //設定以'Asia/Taipei'為時區的時間, 格式是這樣 2023/10/13 02:34:10
  let timex = (new Date()).toLocaleString( 'zh-TW', { 
    timeZone: 'Asia/Taipei',
    hour12: false 
  })
  timex = timex.replaceAll("/", "").replaceAll(":", "").replaceAll(" ", "_")
  //將時間字串 改成這個格式 20231013_023410
  console.log(timex);
  
  // 最後的檔名變成 'youtube_list_'+timex 例如: youtube_list_20231013_023410
  top.saveJSON(JSON.parse(JSON.stringify(this.ytlist)), 'youtube_list_'+timex)
  
  //存檔後變成 youtube_list_20231013_023410.json
},

以上算是將 Vue版的 youtube點播機,使用到 IFrame Player API 來開發
這中間也有穿插使用 jQuery 及 p5.js, w3.css
或許有更好,更簡便的寫法
或是都可以用vue的方式來實作

不過總算是有用到vue的方式

結果是不錯的,Vue版的 youtube點播機 自己也用的很高興

還剩最後一篇了, 加油!!

https://ithelp.ithome.com.tw/upload/images/20231013/20152098tlUAyK0cOY.png

以下是完整的程式碼
index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" href="/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.2/p5.js"></script>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
    <link rel="stylesheet" href="https://www.w3schools.com/w3css/4/w3.css">
    <title>Vue Youtube Player</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.js"></script>

    <script>
      var tag = document.createElement('script');
      tag.src = "https://www.youtube.com/player_api";
      var firstScriptTag = document.getElementsByTagName('script')[0];
      firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
    
      let player;
      function onYouTubePlayerAPIReady() {
        player = new YT.Player('ytplayer', {
          height: '360',
          width: '640',
          videoId: 'bHQqvYy5KYo',
          events: {
            'onReady': onPlayerReady,
            'onStateChange': onPlayerStateChange
          }
        })
      }

      function onPlayerReady(event) {
        event.target.playVideo();
      }

      let done = false;
      let pid = 0;
      function onPlayerStateChange(event) {
        
        console.log(event.data);
        if (event.data == 0 ) {
          done = true;
        }

        if(done){
          done = false;
          $("#app")[0]._vnode.component.ctx.change_yt();
        }
      }

      function stopVideo() {
        player.stopVideo();
      }

    </script>
    <script>
      function setup(){
        noCanvas();
        noLoop();
      }
    </script>
  </body>
</html>

main.js

import { createApp } from 'vue'

import App from './App.vue'
const app = createApp(App)
app.mount('#app')

App.vue

<template>
  <div>
    <h3>Vue YouTube Player</h3>
    <h4>videoID: {{ videoID  }} ---- {{ vt_title  }}</h4>
    Input Youtube Album: <input class="w3-input w3-border w3-round" type="text" v-model="vt_album" @click="vt_album=''">
    Input Youtube Title: <input class="w3-input w3-border w3-round" type="text" v-model="vt_title" @click="vt_title=''">
    Input Youtube URL: <input class="w3-input w3-border w3-round" type="text" v-model="vt_url" @click="vt_url=''">
    <button class="w3-btn w3-border w3-round-large" @click="add_yt">新增</button>
    <button class="w3-btn w3-border w3-round-large" @click="record_yt">修改</button>
    <button class="w3-btn w3-border w3-round-large" @click="delete_yt">刪除</button>
    <button class="w3-btn w3-border w3-round-large" @click="change_yt">下一首</button>
    <button class="w3-btn w3-border w3-round-large" @click="save_yt">下載清單</button>
    上傳清單: <input type="file" class="w3-btn w3-border w3-round-large" id="jsnfile" name="jsnfile" @change="upload_yt" accept="*.json">

  </div>
  <!-- <div>
   <iframe width="640" height="360" :src="ytlink" title="" frameborder="1" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
  </div> -->
  <div>
    <div id="ytplayer"></div>
  </div>
  <table class="w3-table-all">
    <tr><th>videoID</th><th>Video_Title</th><th>Album_Title</th></tr>
    <tr v-for="x in ytlist">
      <td @click="edit_yt(x)" style="cursor:pointer">{{ x.videoID }}</td><td>{{ x.videoTitle }}</td><td>{{ x.albumName }}</td>
    </tr>
  </table>
</template>

<script>
export default {
  data() { 
    return {
       vt_url: "https://www.youtube.com/watch?v=FqCw7gCTZQs&pp=ygUP5LmZ5aWz44Gp44KC44KI",
       vt_album: "Vue YouTube Player",
       vt_title: "",
       videoID: "",
       ytlink: "",
       vid: 0,
       jsn_data: "",
       ytlist: [
        { videoID: "Ptk_1Dc2iPY", videoTitle: "Canon in D - Cello & Piano", albumName: "Canon"},
        { videoID: "7051Y4WVFJA", videoTitle: "2 Hours Of Canon in D by Pachelbel", albumName: "Canon"}
       ]
    };
  },
  methods: {
    add_yt() {
      let str = this.vt_url;
      let urlParams = new URLSearchParams(str.split("?")[1])
      let vid = urlParams.get("v");
      if(vid!=null){
        this.videoID = vid;
      } else {
        let f1 = str.indexOf("?"); 
        if(f1!=-1){
          str = str.split("?")[0];
        } 
        let f2 = str.indexOf("youtu.be"); 
        let f3 = str.indexOf("embed"); 
        let f4 = str.indexOf("shorts"); 
        if(f2!=-1){
          this.videoID = str.split("youtu.be/")[1];
        } else if(f3!=-1){
          this.videoID = str.split("embed/")[1];
        } else if(f4!=-1){
          this.videoID = str.split("shorts/")[1];
        } else if(str.length==11){
          this.videoID = str;
        } else {
          this.videoID = "";
        }
      }
      this.ytlink = "https://www.youtube.com/embed/"+this.videoID+"?autoplay=1"

      player.loadVideoById(this.videoID, 0);

      setTimeout(()=>{ 
          this.vt_title = $("#ytplayer").attr("title"); 
          let yt_index = this.ytlist.findIndex((e) => e.videoID ===  this.videoID )
          if(yt_index == -1 && this.videoID.length==11){
            this.ytlist.push({videoID: this.videoID, videoTitle: this.vt_title, albumName: this.vt_album})
            this.vid = this.ytlist.length-1;
          }

          console.log($("#app")[0]._vnode.component.ctx.ytlist);
      }, 2000);

    },
    record_yt() {
      let yt1 = this.ytlist.find((e) => e.videoID ===  this.videoID )
      yt1.albumName = this.vt_album;
      yt1.videoTitle = this.vt_title;
    },
    /**
    * @param {{ videoID: string; videoTitle: string; albumName: string; }} n
    */
    edit_yt(n){
      console.log(n)
      this.videoID = n.videoID
      this.vt_title = n.videoTitle
      this.vt_album = n.albumName
      this.ytlink = "https://www.youtube.com/embed/"+this.videoID+"?autoplay=1"

      let yt_index = this.ytlist.findIndex((e) => e.videoID ===  this.videoID )
      this.vid = yt_index;
      player.loadVideoById(this.videoID, 0);

      setTimeout(()=>{ 
          this.vt_title = $("#ytplayer").attr("title"); 
          n.videoTitle = this.vt_title
      }, 2000);
    },
    save_yt(){
      console.log(top)
      let timex = (new Date()).toLocaleString( 'zh-TW', { 
        timeZone: 'Asia/Taipei',
        hour12: false 
      })
      timex = timex.replaceAll("/", "").replaceAll(":", "").replaceAll(" ", "_")
      console.log(timex);

      top.saveJSON(JSON.parse(JSON.stringify(this.ytlist)), 'youtube_list_'+timex)
    },
    delete_yt(){
      let yt_index = this.ytlist.findIndex((e) => e.videoID ===  this.videoID )
      if(yt_index != -1){
        this.ytlist.splice(yt_index, 1);
        let v_cnt = this.ytlist.length;
        this.vid = (this.vid)%v_cnt; 
        this.edit_yt(this.ytlist[this.vid])
      }
    },
    change_yt(){
      let v_cnt = this.ytlist.length;
      // let vid = parseInt(Math.random()*v_cnt+"");
      this.vid = (this.vid+1)%v_cnt; 
      this.edit_yt(this.ytlist[this.vid])
    },
    async upload_yt(e){
      const file = e.target.files.item(0)
      this.jsn_data = await file.text();
      console.log(JSON.parse(this.jsn_data))
      this.ytlist = JSON.parse(this.jsn_data)
      this.vid = 0
      this.edit_yt(this.ytlist[this.vid]) 
    }
    
  }
}
</script>

<style>
div {
  padding:10px
}

button {
  margin: 5px;
}

</style>


上一篇
V28_Vue的小專案_youtube點播機(2)
下一篇
V30_Vue的小專案_youtube點播機(4)
系列文
業主說給你30天學會Vue31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言